package org.swxjava.config;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.swxjava.assembler.ConverterManager;
import org.swxjava.config.Configurator;
import org.swxjava.Container;
import org.swxjava.util.Log;
import org.swxjava.util.LogFactory;
import org.swxjava.util.MessageBundle;
import org.swxjava.core.ServiceLocatorManager;
import org.swxjava.config.SwxXmlConfigurator;
import org.swxjava.WebContextFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class SwxXmlConfigurator implements Configurator {

	public static final String PACKAGE = "/org/swxjava";

	public static final String FILE_SWX_XML = PACKAGE + "/swx.xml";

	/** The default swx.xml file path */
	public static final String DEFAULT_SWX_XML = "/WEB-INF/swx.xml";

	/*
	 * The element names
	 */
	private static final String ELEMENT_INIT = "init";

	private static final String ELEMENT_ALLOW = "allow";

	private static final String ELEMENT_SERVICE = "service";

	private static final String ELEMENT_CONVERT = "convert";

	private static final String ELEMENT_PARAM = "param";

	/*
	 * The attribute names
	 */
	private static final String ATTRIBUTE_ID = "id";

	private static final String ATTRIBUTE_CLASS = "class";

	private static final String ATTRIBUTE_LOCATOR = "locator";

	private static final String ATTRIBUTE_CONVERTER = "converter";

	private static final String ATTRIBUTE_MATCH = "match";

	private static final String ATTRIBUTE_NAME = "name";

	private static final String ATTRIBUTE_VALUE = "value";

	/** Logger */
	private static final Log LOG = LogFactory.getLog(SwxXmlConfigurator.class);

	private Document document;

	/**
	 * The properties that we don't warn about if they don't exist.
	 */
	private static List ignore = Arrays.asList(new String[] { "class", });

	private ConverterManager converterManager;

	private ServiceLocatorManager locatorManager;

	private String classResourceName;

	private String servletResourceName;

	public void setServletResourceName(String servletResourceName)
			throws IOException, ParserConfigurationException, SAXException {
		this.servletResourceName = servletResourceName;

		ServletContext servletContext = WebContextFactory.get().getServletContext();
		if (servletContext == null) {
			throw new IOException(MessageBundle.getString("SwxXmlConfigurator.MissingServletContext"));
		}

		InputStream in = null;
		try {
			in = servletContext.getResourceAsStream(servletResourceName);
			if (in == null) {
				throw new IOException(MessageBundle.getString("SwxXmlConfigurator.MissingConfigFile",servletResourceName));
			}
			LOG.debug("Configuring from servlet resource: " + servletResourceName);
			setInputStream(in);
		} finally {
			try {
				in.close();
			} catch (IOException ex) {
				LOG.warn(ex.getMessage(), ex);
			}
		}
	}

	public void setClassResourceName(String classResourceName)
			throws IOException, ParserConfigurationException, SAXException {
		this.classResourceName = classResourceName;

		InputStream in = getClass().getResourceAsStream(classResourceName);
		if (in == null) {
			throw new IOException(MessageBundle.getString("SwxXmlConfigurator.MissingConfigFile", classResourceName));
		}

		LOG.debug("Configuring from class resource: " + classResourceName);
		setInputStream(in);
	}

	public void setInputStream(InputStream in)
			throws ParserConfigurationException, SAXException, IOException {
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		dbf.setValidating(true);
		DocumentBuilder db = dbf.newDocumentBuilder();
		db.setEntityResolver(new DTDEntityResolver());
		document = db.parse(in);
	}

	public void configure(Container container) {
		// retrieve configuration targets
		converterManager = container.getConverterManager();
		locatorManager = container.getServiceLocatorManager();

		Element root = document.getDocumentElement();

		NodeList rootChildren = root.getChildNodes();
		for (int i = 0; i < rootChildren.getLength(); i++) {
			Node node = rootChildren.item(i);
			if (node.getNodeType() == Node.ELEMENT_NODE) {
				Element child = (Element) node;
				if (child.getNodeName().equals(ELEMENT_INIT)) {
					loadInits(child);
				} else if (child.getNodeName().equals(ELEMENT_ALLOW)) {
					loadAllows(child);
				}
			}
		}
	}

	private void loadInits(Element child) {
		NodeList inits = child.getChildNodes();
		for (int j = 0; j < inits.getLength(); j++) {
			if (inits.item(j).getNodeType() == Node.ELEMENT_NODE) {
				Element initer = (Element) inits.item(j);

				if (initer.getNodeName().equals(ATTRIBUTE_LOCATOR)) {
					// init/locator
					String id = initer.getAttribute(ATTRIBUTE_ID);
					String className = initer.getAttribute(ATTRIBUTE_CLASS);
					locatorManager.addServiceLocatorType(id, className);

				} else if (initer.getNodeName().equals(ATTRIBUTE_CONVERTER)) {
					// init/converter
					String id = initer.getAttribute(ATTRIBUTE_ID);
					String className = initer.getAttribute(ATTRIBUTE_CLASS);
					converterManager.addConverterType(id, className);
				}
			}
		}
	}

	private void loadAllows(Element child) {
		NodeList allows = child.getChildNodes();
		for (int j = 0; j < allows.getLength(); j++) {
			if (allows.item(j).getNodeType() == Node.ELEMENT_NODE) {
				Element allower = (Element) allows.item(j);

				if (allower.getNodeName().equals(ELEMENT_SERVICE)) {
					loadService(allower);

				} else if (allower.getNodeName().equals(ELEMENT_CONVERT)) {
					loadConvert(allower);
				}
			}
		}
	}

	private void loadService(Element service) {
		String serviceName = service.getAttribute(ATTRIBUTE_NAME);
		String locatorType = service.getAttribute(ATTRIBUTE_LOCATOR);
		try {
			Map params = createSettingMap(service);
			locatorManager.addService(serviceName, locatorType, params);
		} catch (Exception e) {
			// TODO
			LOG.error(e);
		}
	}

	private void loadConvert(Element allower) {
		String match = allower.getAttribute(ATTRIBUTE_MATCH);
		String type = allower.getAttribute(ATTRIBUTE_CONVERTER);

		try {
			Map params = createSettingMap(allower);
			converterManager.addConverter(match, type, params);
		} catch (NoClassDefFoundError ex) {
			LOG.info("Convertor '" + type + "' not loaded due to NoClassDefFoundError. (match='" + match + "'). Cause: " + ex.getMessage());
		} catch (Exception ex) {
			LOG.error("Failed to add convertor: match=" + match + ", type=" + type, ex);
		}
	}

	private static Map createSettingMap(Element parent) {
		Map params = new HashMap();
		// Go through the param elements in the allower element, adding to the
		// param map
		NodeList locNodes = parent.getElementsByTagName(ELEMENT_PARAM);
		for (int i = 0; i < locNodes.getLength(); i++) {
			Element element = (Element) locNodes.item(i);
			if (element.getParentNode() != parent) {
				continue;
			}

			String name = element.getAttribute(ATTRIBUTE_NAME);
			if (name != null) {
				String value = element.getAttribute(ATTRIBUTE_VALUE);
				if (value == null || value.length() == 0) {
					StringBuffer buffer = new StringBuffer();
					NodeList textNodes = element.getChildNodes();

					for (int j = 0; j < textNodes.getLength(); j++) {
						buffer.append(textNodes.item(j).getNodeValue());
					}

					value = buffer.toString();
				}
				params.put(name, value);
			}
		}
		return params;
	}

	public static class DTDEntityResolver implements EntityResolver {

		public static final Log LOG = LogFactory
				.getLog(DTDEntityResolver.class);

		private static final String[][] MAPPINGS = {
				{ "-//DTD SWX Java 0.1//EN", "/swx-01.dtd" }};

		public InputSource resolveEntity(String publicId, String systemId)
				throws SAXException {
			for (int i = 0; i < MAPPINGS.length; i++) {
				if (publicId.equals(MAPPINGS[i][0])) {
					if (i != MAPPINGS.length - 1) {
						String doctype = "<!DOCTYPE swx PUBLIC \""
								+ MAPPINGS[MAPPINGS.length - 1][0] + "\" \""
								+ MAPPINGS[MAPPINGS.length - 1][1] + "\">";
						LOG.warn("Deprecated public id in swx.xml. Use: " + doctype);
					}
					String dtdname = SwxXmlConfigurator.PACKAGE
							+ MAPPINGS[i][1];
					InputStream raw = getClass().getResourceAsStream(dtdname);
					return new InputSource(raw);
				}
			}
			throw new SAXException(MessageBundle.getString(
					"DTDEntityResolver.ResolveFailed", publicId, systemId));
		}
	}
}
